package my.shadowsocks.server;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.socks.SocksAddressType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

/**
 * @author fengbo
 * @date 2018/9/29
 */
public class HostHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = LoggerFactory.getLogger(HostHandler.class);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf dataBuff = (ByteBuf) msg;

        if (dataBuff.readableBytes() < 2) {
            log.error("client msg to short.");
            return;
        }

        String host = parseHost(dataBuff);
        int port = dataBuff.readShort();

        InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientHost = socketAddress.getAddress().getHostAddress();
        log.info("client host = {}, access address = {}, port = {}.", clientHost, host, port);

        ClientProxyHandler clientProxyHandler = new ClientProxyHandler(dataBuff);
        ctx.channel().pipeline().addLast(clientProxyHandler);
        ctx.channel().pipeline().remove(this);

        Bootstrap bootstrap = new Bootstrap();
        InternetDataHandler internetDataHandler = new InternetDataHandler(clientProxyHandler, dataBuff);
        bootstrap.group(ctx.channel().eventLoop()).channel(EpollSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) {
                        channel.pipeline().addLast(internetDataHandler);
                    }
                });
        bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                clientProxyHandler.setInternetDataHandler(internetDataHandler);
            } else {
                log.info("connect fail host = {}, port = {}", host, port);
                future.cancel(true);
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
        log.error("HostHandler error", cause);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        ctx.close();
        log.info("HostHandler channelInactive close");
    }

    /**
     * 从数据流中解析出主机名
     * @param dataBuff 数据流
     * @return 主机名
     * @throws UnknownHostException 未知主机异常
     */
    private String parseHost(ByteBuf dataBuff) throws UnknownHostException {
        String host;
        int addressType = dataBuff.getUnsignedByte(0);
        if (addressType == SocksAddressType.IPv4.byteValue()) {
            if (dataBuff.readableBytes() < 7) {
                throw new RuntimeException("parse host error");
            }
            dataBuff.readUnsignedByte();
            byte[] ipBytes = new byte[4];
            dataBuff.readBytes(ipBytes);
            host = InetAddress.getByAddress(ipBytes).toString().substring(1);
        } else if (addressType == SocksAddressType.DOMAIN.byteValue()) {
            int hostLen = dataBuff.getUnsignedByte(1);
            if (dataBuff.readableBytes() < hostLen + 4) {
                throw new RuntimeException("parse host error");
            }
            dataBuff.readUnsignedByte();
            dataBuff.readUnsignedByte();
            byte[] hostBytes = new byte[hostLen];
            dataBuff.readBytes(hostBytes);
            host = new String(hostBytes);
        } else {
            log.error("unknown address type: {}", addressType);
            throw new IllegalArgumentException("unknown address type: " + addressType);
        }
        return host;
    }

}
